﻿using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;

namespace KvDbApi
{
    public class KeyValueWriterStream : Stream
    {
        private const int WriteBufferSize = Usage.ValuePartSize * 2;

        private readonly IDbConnection connection;

        private readonly ValueHandle handle;

        [ThreadStatic]
        private static byte[] dataBuffer;

        [ThreadStatic]
        private static BufferedValueCollection dataValue;

        private IDbCommand appendCommand;

        private SqlParameter versionOutput;

        private long totalBytes;

        private int bufferCount;

        private ValueState valueState;

        public KeyValueWriterStream(IDbConnection connection, ValueHandle handle)
        {
            this.handle = handle;
            this.connection = connection;
        }

        public override bool CanRead
        {
            get
            {
                return false;
            }
        }

        public override bool CanSeek
        {
            get
            {
                return false;
            }
        }

        public override bool CanWrite
        {
            get
            {
                return true;
            }
        }

        public override long Length
        {
            get
            {
                throw new NotSupportedException("Total length is not known for a forward only, write only stream.");
            }
        }

        public override long Position
        {
            get
            {
                return this.totalBytes;
            }

            set
            {
                throw Usage.SeekNotSupportedException();
            }
        }

        public override void Flush()
        {
            if (this.bufferCount == 0)
            {
                return;
            }

            try
            {
                dataValue.Max = this.bufferCount;
                if (this.valueState == ValueState.Unknown)
                {
                    this.WriteSet();
                }
                else
                {
                    this.WriteAppend();
                }
            }
            finally
            {
                this.bufferCount = 0;
            }
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotSupportedException("This is a write only stream.");
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw Usage.SeekNotSupportedException();
        }

        public override void SetLength(long value)
        {
            throw Usage.SeekNotSupportedException();
        }

        public override void WriteByte(byte value)
        {
            if (this.valueState == ValueState.NotFound)
            {
                return;
            }

            CheckBuffer();
            dataBuffer[this.bufferCount] = value;
            this.bufferCount += 1;
            this.totalBytes += 1;
            if (this.bufferCount == WriteBufferSize)
            {
                this.Flush();
            }
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (this.valueState == ValueState.NotFound)
            {
                return;
            }

            buffer.ValidateForBufferOperation(offset, count);
            while (count > 0)
            {
                var size = this.CopyToSqlBuffer(buffer, offset, count);
                if (this.bufferCount == WriteBufferSize)
                {
                    this.Flush();
                }

                this.totalBytes += size;
                count -= size;
                offset += size;
            }
        }

        public override void Close()
        {
            if (this.bufferCount > 0)
            {
                this.Flush();
            }

            if (this.appendCommand != null)
            {
                this.appendCommand.Dispose();
                this.appendCommand = null;
            }
        }

        private static void CheckBuffer()
        {
            if (dataBuffer == null)
            {
                // Threadstatic buffers. The underlying enumerator is created on each call to GetEnumerator.
                // The enumerator instance is stateful and not static (nor threadstatic).
                dataBuffer = new byte[WriteBufferSize];
                dataValue = new BufferedValueCollection(dataBuffer);
            }
        }

        private int CopyToSqlBuffer(byte[] buffer, int offset, int count)
        {
            CheckBuffer();
            var size = Math.Min(WriteBufferSize - this.bufferCount, count);
            Array.Copy(buffer, offset, dataBuffer, this.bufferCount, size);
            this.bufferCount += size;
            return size;
        }

        private void WriteAppend()
        {
            if (this.appendCommand == null)
            {
                var command = this.connection.CreateKvAppend(
                    this.handle,
                    dataValue,
                    out this.versionOutput);

                command.Prepare();
                this.appendCommand = command;
            }

            this.appendCommand.ExecuteNonQuery();
            this.handle.SetVersionMetadata(this.versionOutput);
        }

        private void WriteSet()
        {
            this.valueState = this.TryMemorySet();
            if (this.valueState != ValueState.NotFound)
            {
                return;
            }

            // If not found, try to move:
            this.valueState = this.TryMemoryMove();
            if (this.valueState == ValueState.NotFound)
            {
                // If a version was provided and not able to be moved, nullify the version, and set as insert.
                // NOTE: This is a design opinion that must be understood by the consumer.
                this.handle.NullifyVersion();
                this.valueState = ValueState.NotFound;
                return;
            }

            // Reinitialize the buffer enumerator to set again. The underlying bytes are still written into the threadstatic buffer.
            this.valueState = this.TryMemorySet();
        }

        private ValueState TryMemorySet()
        {
            SqlParameter newVersionOutput;
            SqlParameter expiryOutput;
            using (var command = this.connection.CreateKvSet(
                this.handle,
                dataValue,
                out newVersionOutput,
                out expiryOutput))
            {
                command.ExecuteNonQuery();
                this.handle.SetValueMetadata(ValueState.Memory, newVersionOutput, expiryOutput);
                return this.handle.LastKnownState;
            }
        }

        private ValueState TryMemoryMove()
        {
            SqlParameter newVersionOutput;
            SqlParameter expiryOutput;
            using (var command = this.connection.CreateKvMove(
                this.handle,
                out newVersionOutput,
                out expiryOutput))
            {
                command.ExecuteNonQuery();
                this.handle.SetValueMetadata(ValueState.Memory, newVersionOutput, expiryOutput);
                return this.handle.LastKnownState;
            }
        }
    }
}
